﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;

namespace AsyncEvtEcho
{
    class EchoClient : IDisposable
    {
        // *************************************************************** //
        #region [ member fields ]

        private readonly Socket m_socket;

        /// <summary>
        /// used for connect and receive
        /// </summary>
        private readonly SocketAsyncEventArgs m_recvEvtArgs;

        /// <summary>
        /// chekanote: used for send, because client must send and receive at the same time
        /// we have to use two seperate SocketAsyncEventArgs, one for reading, the other for writing
        /// (for simplicity and reusage, server will read and write sequentially, lauch another async-read
        /// only after the reply has already been sent back; client must do both concurrently, but one advantage
        /// for client is it has no need to consider reusage, because it only connects to one server)
        /// </summary>
        private readonly SocketAsyncEventArgs m_sendEvtArgs;

        private readonly HeadBodyRecver m_recvFSM;

        private IEnumerator<string> m_msgIterator;
        private readonly int m_cmd;

        #endregion

        // *************************************************************** //
        #region [ constructor ]

        public EchoClient(int cmd)
        {
            m_cmd = cmd;

            m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            m_recvEvtArgs = new SocketAsyncEventArgs();
            m_recvEvtArgs.Completed += this.OnReceived;

            m_sendEvtArgs = new SocketAsyncEventArgs();
            m_sendEvtArgs.Completed += this.OnSent;

            m_recvFSM = new HeadBodyRecver();
        }

        #endregion

        // *************************************************************** //
        #region [ destructor ]

        private bool m_disposed;

        ~EchoClient()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (disposing)
                {
                    m_socket.Shutdown(SocketShutdown.Both);
                    m_socket.Close();

                    m_recvEvtArgs.Completed -= this.OnReceived;
                    m_recvEvtArgs.Dispose();

                    m_sendEvtArgs.Completed -= this.OnSent;
                    m_sendEvtArgs.Dispose();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }
        #endregion

        // *************************************************************** //
        #region [ public APIs ]

        public void Run(string ipAddress, int port, IEnumerable<string> messages)
        {
            m_msgIterator = messages.GetEnumerator();

            m_recvEvtArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
            m_socket.ConnectAsync(m_recvEvtArgs);
        }

        #endregion

        // *************************************************************** //
        #region [ private helpers ]

        private void OnReceived(object sender, SocketAsyncEventArgs evtargs)
        {
            switch (evtargs.LastOperation)
            {
                case SocketAsyncOperation.Connect:
                    ProcessConnect(evtargs);
                    break;

                case SocketAsyncOperation.Receive:
                    ProcessReceive(evtargs);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("un-recognized operation");
            }
        }

        private void ProcessConnect(SocketAsyncEventArgs evtargs)
        {
            if (evtargs.SocketError == SocketError.Success)
            {
                Console.WriteLine("server connected");
                m_recvFSM.StartRecvSocket(m_socket, m_recvEvtArgs, ProcessReceive);
                StartSend();
            }
            else
                throw new SocketException((int)evtargs.SocketError);
        }

        private void ProcessReceive(SocketAsyncEventArgs evtargs)
        {
            if (evtargs.BytesTransferred > 0 && evtargs.SocketError == SocketError.Success)
            {
                if (m_recvFSM.Receive(evtargs.Buffer, evtargs.Offset, evtargs.BytesTransferred))
                {
                    Tuple<Header, byte[]> replied = m_recvFSM.LastPacket;
                    Console.WriteLine("{0}:{1}",
                        Utility.GetCmdDescription(replied.Item1.Command),
                        Encoding.ASCII.GetString(replied.Item2));
                }
                m_recvFSM.StartRecvSocket(m_socket, m_recvEvtArgs, ProcessReceive);
            }
            else if (evtargs.BytesTransferred == 0)
            {
                Console.WriteLine("server active close");
                Dispose();
            }
            else if (evtargs.SocketError != SocketError.Success)
            {
                Console.WriteLine("!!! receive error from server: {0}", new SocketException((int)evtargs.SocketError).Message);
                Dispose();
            }
            else
                throw new NotImplementedException("unhandled receive case");
        }

        private void StartSend()
        {
            if (m_msgIterator.MoveNext())
            {
                byte[] requestBuffer = m_msgIterator.Current.MakePacket(m_cmd).CombineBinary();

                m_sendEvtArgs.SetBuffer(requestBuffer, 0, requestBuffer.Length);
                if (!m_socket.SendAsync(m_sendEvtArgs))
                    OnSent(null, m_sendEvtArgs);
            }
            else
                Console.WriteLine("****************** ALL SENT ******************");
        }

        private void OnSent(object sender, SocketAsyncEventArgs evtargs)
        {
            if (evtargs.SocketError == SocketError.Success)
            {
                StartSend();
            }
            else
                throw new SocketException((int)evtargs.SocketError);
        }

        #endregion
    }
}
